using System;
using System.Data;
using System.Data.Common;
using System.Collections.Generic;
using NBear.Common;

namespace NBear.Data
{
    /// <devdoc>
    /// CachingMechanism provides caching support for stored procedure 
    /// parameter discovery and caching
    /// </devdoc>
    internal class CachingMechanism
    {
        #region Private Members

        private Dictionary<string, IDataParameter[]> paramCache = new Dictionary<string, IDataParameter[]>();

        private static string CreateHashKey(string connectionString, string storedProcedure)
        {
            return connectionString + ":" + storedProcedure;
        }

        #endregion

        #region Public Members

        /// <devdoc>
        /// Create and return a copy of the IDataParameter array.
        /// </devdoc>        
        public static IDataParameter[] CloneParameters(IDataParameter[] originalParameters)
        {
            Check.Require(originalParameters != null);

            IDataParameter[] clonedParameters = new IDataParameter[originalParameters.Length];

            for (int i = 0, j = originalParameters.Length; i < j; i++)
            {
                clonedParameters[i] = (IDataParameter)((ICloneable)originalParameters[i]).Clone();
            }

            return clonedParameters;
        }

        /// <devdoc>
        /// Empties all items from the cache
        /// </devdoc>
        public void Clear()
        {
            this.paramCache.Clear();
        }

        /// <devdoc>
        /// Add a parameter array to the cache for the command.
        /// </devdoc>        
        public void AddParameterSetToCache(IDbCommand command, IDataParameter[] parameters)
        {
            string connectionString = command.Connection.ConnectionString;
            string storedProcedure = command.CommandText;
            string key = CreateHashKey(connectionString, storedProcedure);
            lock (paramCache)
            {
                if (!paramCache.ContainsKey(key))
                {
                    this.paramCache.Add(key, parameters);
                }
            }
        }

        /// <devdoc>
        /// Gets a parameter array from the cache for the command. Returns null if no parameters are found.
        /// </devdoc>        
        public IDataParameter[] GetCachedParameterSet(IDbCommand command)
        {
            Check.Require(command != null);

            string connectionString = command.Connection.ConnectionString;
            string storedProcedure = command.CommandText;
            string key = CreateHashKey(connectionString, storedProcedure);
            if (!paramCache.ContainsKey(key))
            {
                return null;
            }
            else
            {
                IDataParameter[] cachedParameters = this.paramCache[key];
                Check.Invariant(cachedParameters != null);
                return CloneParameters(cachedParameters);
            }
        }

        /// <devdoc>
        /// Gets if a given stored procedure on a specific connection string has a cached parameter set
        /// </devdoc>        
        public bool IsParameterSetCached(IDbCommand command)
        {
            Check.Require(command != null);

            string hashKey = CreateHashKey(
                command.Connection.ConnectionString,
                command.CommandText);
            return paramCache.ContainsKey(hashKey);
        }

        #endregion

        public bool IsParameterSetCached(DbCommand command, string[] paramNames)
        {
            Check.Require(command != null);

            string paramNameStr = "";
            if (paramNames != null && paramNames.Length > 0)
            {
                foreach (string pn in paramNames)
                {
                    paramNameStr += "_" + pn;
                }
            }

            string hashKey = CreateHashKey(
                command.Connection.ConnectionString,
                command.CommandText + paramNameStr);
            return paramCache.ContainsKey(hashKey);
        }

        public void AddParameterSetToCache(DbCommand command, string[] paramNames, IDataParameter[] parameters)
        {
            string connectionString = command.Connection.ConnectionString;
            string storedProcedure = command.CommandText;
            string paramNameStr = "";
            if (paramNames != null && paramNames.Length > 0)
            {
                foreach (string pn in paramNames)
                {
                    paramNameStr += "_" + pn;
                }
            }

            string key = CreateHashKey(connectionString, storedProcedure + paramNameStr);
            lock (paramCache)
            {
                if (!paramCache.ContainsKey(key))
                {
                    this.paramCache.Add(key, parameters);
                }
            }
        }

        public IDataParameter[] GetCachedParameterSet(DbCommand command, string[] paramNames)
        {
            Check.Require(command != null);

            string connectionString = command.Connection.ConnectionString;
            string storedProcedure = command.CommandText;
            string paramNameStr = "";
            if (paramNames != null && paramNames.Length > 0)
            {
                foreach (string pn in paramNames)
                {
                    paramNameStr += "_" + pn;
                }
            }
            string key = CreateHashKey(connectionString, storedProcedure + paramNameStr);
            if (!paramCache.ContainsKey(key))
            {
                return null;
            }
            else
            {
                IDataParameter[] cachedParameters = this.paramCache[key];
                Check.Invariant(cachedParameters != null);
                return CloneParameters(cachedParameters);
            }
        }
    }

    /// <summary>
    /// <para>
    /// Provides parameter caching services for dynamic parameter discovery of stored procedures.
    /// Eliminates the round-trip to the database to derive the parameters and types when a command
    /// is executed more than once.
    /// </para>
    /// </summary>
    public class ParameterCache
    {
        private CachingMechanism cache = new CachingMechanism();

        /// <summary>
        /// <para>Empty the parameter cache</para>
        /// </summary>
        public void Clear()
        {
            this.cache.Clear();
        }

        /// <summary>
        /// <para>Checks to see if a cache entry exists for a specific command on a specific connection</para>
        /// </summary>
        /// <param name="command">
        /// <para>The command to check.</para>
        /// </param>
        /// <returns>True if the parameters are already cached for the provided command, false otherwise</returns>
        public virtual bool AlreadyCached(DbCommand command)
        {
            Check.Require(command != null);

            return this.cache.IsParameterSetCached(command);
        }

        /// <summary>
        /// <para>Adds parameters to a command using the cache</para>
        /// </summary>
        /// <param name="command">
        /// <para>The command to add the parameters.</para>
        /// </param>
        public virtual void AddParametersFromCache(DbCommand command)
        {
            Check.Require(command != null);

            IDataParameter[] parameters = this.cache.GetCachedParameterSet(command);

            foreach (IDataParameter p in parameters)
            {
                command.Parameters.Add(p);
            }
        }

        /// <summary>
        /// Caches the parameters.
        /// </summary>
        /// <param name="command">The command.</param>
        public virtual void CacheParameters(DbCommand command)
        {
            Check.Require(command != null);

            cache.AddParameterSetToCache(command, CreateParameterCopy(command));
        }

        /// <summary>
        /// Creates the parameter copy.
        /// </summary>
        /// <param name="command">The command.</param>
        /// <returns></returns>
        private static IDataParameter[] CreateParameterCopy(DbCommand command)
        {
            IDataParameterCollection parameters = command.Parameters;
            IDataParameter[] parameterArray = new IDataParameter[parameters.Count];
            parameters.CopyTo(parameterArray, 0);

            return CachingMechanism.CloneParameters(parameterArray);
        }

        public bool AlreadyCached(DbCommand cmd, string[] paramNames)
        {
            Check.Require(cmd != null);

            return this.cache.IsParameterSetCached(cmd, paramNames);
        }

        public void CacheParameters(DbCommand cmd, string[] paramNames)
        {
            Check.Require(cmd != null);

            cache.AddParameterSetToCache(cmd, paramNames, CreateParameterCopy(cmd));
        }
        public void AddParametersFromCache(DbCommand cmd, string[] paramNames)
        {
            Check.Require(cmd != null);

            IDataParameter[] parameters = this.cache.GetCachedParameterSet(cmd, paramNames);

            foreach (IDataParameter p in parameters)
            {
                cmd.Parameters.Add(p);
            }
        }
    }
}